using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

namespace CustomNetworking
{
    /// <summary> 
    /// A StringSocket is a wrapper around a Socket.  It provides methods that
    /// asynchronously read lines of text (strings terminated by newlines) and 
    /// write strings. (As opposed to Sockets, which read and write raw bytes.)  adfasdf
    ///
    /// StringSockets are thread safe.  This means that two or more threads may
    /// invoke methods on a shared StringSocket without restriction.  The
    /// StringSocket takes care of the synchonization.
    /// 
    /// Each StringSocket contains a Socket object that is provided by the client.  
    /// A StringSocket will work properly only if the client refains from calling
    /// the contained Socket's read and write methods.
    /// 
    /// If we have an open Socket s, we can create a StringSocket by doing
    /// 
    ///    StringSocket ss = new StringSocket(s, new UTF8Encoding());
    /// 
    /// We can write a string to the StringSocket by doing
    /// 
    ///    ss.BeginSend("Hello world", callback, payload);
    ///    
    /// where callback is a SendCallback (see below) and payload is an arbitrary object.
    /// This is a non-blocking, asynchronous operation.  When the StringSocket has 
    /// successfully written the string to the underlying Socket, or failed in the 
    /// attempt, it invokes the callback.  The parameters to the callback are a
    /// (possibly null) Exception and the payload.  If the Exception is non-null, it is
    /// the Exception that caused the send attempt to fail.
    /// 
    /// We can read a string from the StringSocket by doing
    /// 
    ///     ss.BeginReceive(callback, payload)
    ///     
    /// where callback is a ReceiveCallback (see below) and payload is an arbitrary object.
    /// This is non-blocking, asynchronous operation.  When the StringSocket has read a
    /// string of text terminated by a newline character from the underlying Socket, or
    /// failed in the attempt, it invokes the callback.  The parameters to the callback are
    /// a (possibly null) string, a (possibly null) Exception, and the payload.  Either the
    /// string or the Exception will be non-null, but nor both.  If the string is non-null, 
    /// it is the requested string (with the newline removed).  If the Exception is non-null, 
    /// it is the Exception that caused the send attempt to fail.
    /// </summary>

    public class StringSocket
    {
        //The Socket trough which we are connecting
        private Socket socket;
        //The encoding style which is to be used.
        private Encoding encoding;
        //The outgoing butter
        private byte[] outGoingBuffer;
        //the payload


        //bool to keep track of when items are queued to be sent
        private bool sendIsOnGoing;
        //bool to keep track of when recieve requests are queued
        private bool receiveIsOnGoing;
        //object to lock methods for multi-threading
        private object sendSync, receiveSync, receiveQueueSync,receiveStringSync;
        //queue to hold items to be sent
        private Queue<Tuple<string, SendCallback, object>> sendStringQueue;
        //queue to hold recieve requests
        private Queue<Tuple<ReceiveCallback, object>> receiveCallBackQueue;
        private Queue<string> receiveStringQueue;
        //incoming buffer byte array
        private byte[] incomingBuffer;
        //receive buffer byte array
        private byte[] receiveBuffer;
        //the string to be returned.
        private string receiveString;
        private int receiveRequestsSent;
        private bool waitingOnSocketReceiveCallback;




        // These delegates describe the callbacks that are used for sending and receiving strings.
        public delegate void SendCallback(Exception e, object payload);
        public delegate void ReceiveCallback(String s, Exception e, object payload);

        /// <summary>
        /// Creates a StringSocket from a regular Socket, which should already be connected.  
        /// The read and write methods of the regular Socket must not be called after the
        /// LineSocket is created.  Otherwise, the StringSocket will not behave properly.  
        /// The encoding to use to convert between raw bytes and strings is also provided.
        /// </summary>
        public StringSocket(Socket s, Encoding e)
        {
            //set socket and encoding
            socket = s;
            encoding = e;
            //initialize buffer
            incomingBuffer = new byte[1024];


            //initalize send buffer
            outGoingBuffer = new byte[1024];
            //initalize queues
            sendStringQueue = new Queue<Tuple<string, SendCallback, object>>();
            receiveCallBackQueue = new Queue<Tuple<ReceiveCallback, object>>();
            receiveStringQueue = new Queue<string>();
            //initalize recieve string
            receiveString = "";
            //intitailize send string object
            sendSync = new object();
            receiveSync = new object();
            receiveStringSync = new object();
            receiveQueueSync = new object();
            receiveRequestsSent = 0;
            waitingOnSocketReceiveCallback = false;
            
            ThreadPool.SetMinThreads(3, 3);

            
          

        }

        /// <summary>
        /// We can write a string to a StringSocket ss by doing
        /// 
        ///    ss.BeginSend("Hello world", callback, payload);
        ///    
        /// where callback is a SendCallback (see below) and payload is an arbitrary object.
        /// This is a non-blocking, asynchronous operation.  When the StringSocket has 
        /// successfully written the string to the underlying Socket, or failed in the 
        /// attempt, it invokes the callback.  The parameters to the callback are a
        /// (possibly null) Exception and the payload.  If the Exception is non-null, it is
        /// the Exception that caused the send attempt to fail. 
        /// 
        /// This method is non-blocking.  This means that it does not wait until the string
        /// has been sent before returning.  Instead, it arranges for the string to be sent
        /// and then returns.  When the send is completed (at some time in the future), the
        /// callback is called on another thread.
        /// 
        /// This method is thread safe.  This means that multiple threads can call BeginSend
        /// on a shared socket without worrying around synchronization.  The implementation of
        /// BeginSend must take care of synchronization instead.  On a given StringSocket, each
        /// string arriving via a BeginSend method call must be sent (in its entirety) before
        /// a later arriving string can be sent.
        /// </summary>
        public void BeginSend(String s, SendCallback callback, object payload)
        {
            lock (sendSync)
            {
                lock (sendStringQueue)
                {
                    sendStringQueue.Enqueue(new Tuple<string, SendCallback, object>(s, callback, payload));
                }
                if (!sendIsOnGoing)
                {
                    sendIsOnGoing = true;
                    outGoingBuffer = encoding.GetBytes(sendStringQueue.First().Item1);
                    socket.BeginSend(outGoingBuffer, 0, outGoingBuffer.Length, SocketFlags.None, MessageSent, new object());
                }




            }

        }
        private void MakeSendCallback(SendCallback callback, Exception e, object o)
        {
            Task.Factory.StartNew(() => callback(e, 0));
        }

        private void MessageSent(IAsyncResult result)
        {
            lock (sendSync)
            {

                // Find out how many bytes were actually sent
                int sentBytes = socket.EndSend(result);

                // Get exclusive access to send mechanism

                // Get the bytes that we attempted to send
                // byte[] sentOutBuffer = (byte[])result.AsyncState;

                // The socket has been closed
                if (sentBytes == 0)
                {
                    socket.Close();
                    Console.WriteLine("Socket closed");
                }

                // Prepend the unsent bytes and try sending again.
                if (sentBytes != outGoingBuffer.Length)
                {
                    //Remove the bytes that have been sent.
                    outGoingBuffer = outGoingBuffer.Skip(sentBytes).ToArray();
                    //resend!
                    socket.BeginSend(outGoingBuffer, 0, outGoingBuffer.Length, SocketFlags.None, MessageSent, new object());
                }
                //if all the bytes were sent successfully we can send set up a callback to the main caller, and get the next item if applicable.
                if (sentBytes == outGoingBuffer.Length)
                {
                    //create a temp tuple to hold the queued item.
                    Tuple<string, SendCallback, object> temp;
                    lock(sendStringQueue)
                    {
                    temp = sendStringQueue.Dequeue();
                    }

                    //If there is more items in the queue to be sent
                    if (sendStringQueue.Count > 0)
                    {
                        //reset the bufer
                        outGoingBuffer = new byte[1024];
                        //callback to main class
                        MakeSendCallback(temp.Item2, null, temp.Item3);
                        //peak at the next item to get it's contents
                        lock (sendStringQueue)
                        {
                            temp = sendStringQueue.Peek();
                        }
                        //yep, still sending
                        sendIsOnGoing = true;
                        //fill the buffer with bytes
                        outGoingBuffer = encoding.GetBytes(temp.Item1);
                        //send bytes to the socket
                        socket.BeginSend(outGoingBuffer, 0, outGoingBuffer.Length, SocketFlags.None, MessageSent, new object());

                    }
                    //the queue is empty, so call back and reset.
                    else
                    {
                        //reset buffer
                        outGoingBuffer = new byte[1024];
                        //set callback and send and set sendIsOngoing to false.
                        SendCallback tempCallBack = temp.Item2;
                        MakeSendCallback(tempCallBack, null, temp.Item3);
                        sendIsOnGoing = false;
                    }
                }
            }
        }



        /// <summary>
        /// We can read a string from the StringSocket by doing
        /// 
        ///     ss.BeginReceive(callback, payload)
        ///     
        /// where callback is a ReceiveCallback (see below) and payload is an arbitrary object.
        /// This is non-blocking, asynchronous operation.  When the StringSocket has read a
        /// string of text terminated by a newline character from the underlying Socket, or
        /// failed in the attempt, it invokes the callback.  The parameters to the callback are
        /// a (possibly null) string, a (possibly null) Exception, and the payload.  Either the
        /// string or the Exception will be non-null, but nor both.  If the string is non-null, 
        /// it is the requested string (with the newline removed).  If the Exception is non-null, 
        /// it is the Exception that caused the send attempt to fail.
        /// 
        /// This method is non-blocking.  This means that it does not wait until a line of text
        /// has been received before returning.  Instead, it arranges for a line to be received
        /// and then returns.  When the line is actually received (at some time in the future), the
        /// callback is called on another thread.
        /// 
        /// This method is thread safe.  This means that multiple threads can call BeginReceive
        /// on a shared socket without worrying around synchronization.  The implementation of
        /// BeginReceive must take care of synchronization instead.  On a given StringSocket, each
        /// arriving line of text must be passed to callbacks in the order in which the corresponding
        /// BeginReceive call arrived.
        /// 
        /// Note that it is possible for there to be incoming bytes arriving at the underlying Socket
        /// even when there are no pending callbacks.  StringSocket implementations should refrain
        /// from buffering an unbounded number of incoming bytes beyond what is required to service
        /// the pending callbacks.
        /// </summary>
        public void BeginReceive(ReceiveCallback callback, object payload)
        {

            lock (receiveQueueSync)
            {
                //enqueue the recieve request
                receiveCallBackQueue.Enqueue(new Tuple<ReceiveCallback, object>(callback, payload));
                //if we are not currently sending
            }
            if (!receiveIsOnGoing)
            {
                receiveIsOnGoing = true;
                ReceiveCheck();

            }
        }

        private void ReceiveCheck()
        {
            if ((receiveStringQueue.Count) > 0 && receiveCallBackQueue.Count > 0)
            {
                ReceiveCallbackLoop();
               
            }
            else if (receiveCallBackQueue.Count > 0 && !waitingOnSocketReceiveCallback)
            {
                waitingOnSocketReceiveCallback = true;
                receiveBuffer = new byte[1024];
                socket.BeginReceive(receiveBuffer, 0, receiveBuffer.Length, SocketFlags.None, MessageReceived, receiveBuffer);
            }
            else if (receiveCallBackQueue.Count == 0)
            {
                receiveIsOnGoing = false;
                
            }
        }


        private void MakeReceiveCallBack(ReceiveCallback callback, string s, Exception e, object o)
        {       
            ThreadPool.QueueUserWorkItem((x) => callback(s, e, o));
        }


        private void ReceiveCallbackLoop()
        {
            while (receiveCallBackQueue.Count > 0 && receiveStringQueue.Count > 0)
            {

                // a temp string to hold the incoming string
                Tuple<ReceiveCallback, object> temp;
                string tempString;
                lock (receiveQueueSync)
                {
                    //temp tuple to hold the dequeued request
                    temp = receiveCallBackQueue.Dequeue();
                }
                lock (receiveStringQueue)
                {
                    tempString = receiveStringQueue.Dequeue();
                }
                MakeReceiveCallBack(temp.Item1, tempString, null, temp.Item2);

            }
            ReceiveCheck();
        }

        private void MessageReceived(IAsyncResult result)
        {

            
            lock (receiveSync)
            {
                // Get the buffer to which the data was written.
                // byte[] buffer = (byte[])(result.AsyncState);

                // Figure out how many bytes have come in
                int bytes = socket.EndReceive(result);

                // If no bytes were received, it means the client closed its side of the socket.
                // Report that to the console and close our socket.
                if (bytes == 0)
                {
                    Console.WriteLine("Socket closed");
                    socket.Close();
                }
                else
                {
                    receiveString += encoding.GetString(receiveBuffer,0,bytes);
                    int index;
                    while ((index = receiveString.IndexOf("\n")) >= 0)
                    {
                        lock (receiveQueueSync)
                        {
                            receiveStringQueue.Enqueue(receiveString.Substring(0, index));
                        }
                        receiveString = receiveString.Substring(index + 1); 
                    }
                    waitingOnSocketReceiveCallback = false;
                    ReceiveCheck();
                }
            }
            
        }
    }
}